package com.dzmsoft.drools.core.kie;

import com.dzmsoft.drools.core.cache.CacheService;
import com.dzmsoft.drools.core.common.Constants;
import com.dzmsoft.drools.core.context.SpringContextHolder;
import com.dzmsoft.drools.core.listener.DefaultAgendaEventListener;
import com.dzmsoft.drools.core.listener.DefaultProcessEventListener;
import com.dzmsoft.drools.core.listener.DefaultRuleRuntimeEventListener;
import com.dzmsoft.drools.core.util.FileUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.drools.decisiontable.InputType;
import org.drools.decisiontable.SpreadsheetCompiler;
import org.kie.api.KieBase;
import org.kie.api.KieBaseConfiguration;
import org.kie.api.builder.Message;
import org.kie.api.builder.Results;
import org.kie.api.conf.EventProcessingOption;
import org.kie.api.io.ResourceType;
import org.kie.api.runtime.KieSession;
import org.kie.internal.utils.KieHelper;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.DigestUtils;

import java.io.*;
import java.util.ArrayList;
import java.util.List;

/**
 * drools操作的模板类
 */
@Slf4j
public class KieTemplate extends KieAccessor implements BeanClassLoaderAware {

    public KieTemplate(String path,String mode, String cacheType){
        this.setPath(path);
        this.setMode(mode);
        this.setCacheType(cacheType);
        this.cacheService = SpringContextHolder.getBean(getCacheType()+ Constants.CACHE_TYPE);
    }

    private ClassLoader classLoader;

    private CacheService cacheService;

    @Override
    public void setBeanClassLoader(ClassLoader classLoader) {
        this.classLoader = classLoader;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        super.afterPropertiesSet();
        if (classLoader == null) {
            classLoader = getClass().getClassLoader();
        }
    }

    /**
     * 通过规则文件名称，获取KieSession
     * @param ruleFileNames 规则文件的名称,可以有多个
     * @return
     */
    public KieSession getKieSession(List<String> ruleFileNames){
        if (CollectionUtils.isEmpty(ruleFileNames)){
            throw new NullPointerException("ruleFileNames is null");
        }
        List<String> rules = new ArrayList<>();
        Long start = System.currentTimeMillis();
        for (String ruleFileName:ruleFileNames){
            // 从缓存中获取内容
            String rule = cacheService.get(ruleFileName);
            if (StringUtils.isBlank(rule)){
                // 缓存中没有取到数据,这种情况是针对新增的文件，在cache中还没有存在,定时任务还没有刷到,则主动加载
                rule = loadAppointRule(ruleFileName);
                rules.add(rule);
            } else {
                if (Constants.ON.equals(cacheService.get(ruleFileName+Constants.FLAG))){
                    // 缓存没有变化,就不需要再校验了，因为loadAppointRule的时候已经verify了
                    rules.add(rule);
                    log.info("文件：{},缓存命中",ruleFileName);
                } else{
                    log.info("文件：{},缓存没有命中",ruleFileName);
                    // 缓存发生变化,需要再次verify
                    if (verify(rule)){
                        // 校验通过的标志
                        cacheService.put(ruleFileName+Constants.FLAG,Constants.ON);
                        rules.add(rule);
                    } else{
                        throw new IllegalArgumentException("ruleFileName verify error");
                    }
                }
            }
        }
        Long end = System.currentTimeMillis();
        log.info("load rule 总耗时:{}ms",end-start);
        // 构建KieSession
        start = System.currentTimeMillis();
        KieSession kieSession = buildKieSession(rules);
        end = System.currentTimeMillis();
        log.info("buildKieSession 总耗时:{}ms",end-start);
        return kieSession;
    }

    /**
     * 校验规则是正常
     * @param rule
     */
    private boolean verify(String rule){
        KieHelper kieHelper = new KieHelper();
        kieHelper.addContent(rule, ResourceType.DRL);
        boolean flag = true;
        try{
            Results results = kieHelper.verify();
            if (results.hasMessages(Message.Level.WARNING, Message.Level.ERROR)) {
                List<Message> messages = results.getMessages(Message.Level.WARNING, Message.Level.ERROR);
                for (Message message : messages) {
                    log.error("Error: {}", message.getText());
                }
                flag = false;
            }
        } catch (Throwable e){
            log.error("kieHelper.verify throwable : {}", e.getMessage());
            flag = false;
        }
        return flag;
    }

    /**
     * 将规则文件转化为kiession
     * 因为定时任务和指定的规则，都已经verify了，所以这里不用再校验了
     * @param rules
     * @return
     */
    private KieSession buildKieSession(List<String> rules){
        KieHelper kieHelper = new KieHelper();
        for (String rule : rules) {
            kieHelper.addContent(rule, ResourceType.DRL);
        }
        KieBaseConfiguration config = kieHelper.ks.newKieBaseConfiguration();

        if (EventProcessingOption.STREAM.getMode().equalsIgnoreCase(getMode())) {
            config.setOption(EventProcessingOption.STREAM);
        } else {
            config.setOption(EventProcessingOption.CLOUD);
        }
        KieBase kieBase = kieHelper.build(config);
        KieSession kieSession = kieBase.newKieSession();
        if (getListener() == null || !Constants.OFF.equalsIgnoreCase(getListener())) {
            kieSession.addEventListener(new DefaultRuleRuntimeEventListener());
            kieSession.addEventListener(new DefaultAgendaEventListener());
            kieSession.addEventListener(new DefaultProcessEventListener());
        }
        return kieSession;
    }

    /**
     * 加载指定的规则文件
     * TODO 这个文件遍历应该还可以优化
     * @param fileName
     * @return
     */
    private String loadAppointRule(String fileName) {
        List<File> fileList = getAllRuleFiles();
        for (File file : fileList) {
            if (fileName.equals(file.getName())) {
                String rule = encodeToString(file.getPath());
                // 校验加载的规则文件是否合法
                if (verify(rule)){
                    // 将正确的规则文件写入到缓存中
                    cacheService.put(fileName, rule);
                    // 校验通过的标志
                    cacheService.put(fileName+Constants.FLAG,Constants.ON);
                } else{
                    log.error("规则文件：{}，存在异常,请注意核查",fileName);
                }
                return rule;
            }
        }
        return null;
    }

    /**
     * 获取spring.drools.path中所有文件
     * @return
     */
    private List<File> getAllRuleFiles(){
        String pathTotal = getPath();
        if (pathTotal == null || pathTotal.length() == 0) {
            Assert.notNull(pathTotal, "path must be not null");
        }
        // TODO 遍历目录下所有的文件，这个地方或许还可以优化
        String[] pathArray = pathTotal.split(KieAccessor.PATH_SPLIT);
        List<File> fileList = new ArrayList<>();
        for (String path : pathArray) {
            FileUtil.fileList(path, fileList);
        }
        return fileList;
    }

    /**
     * 定时任务会加载规则文件,
     */
    public void loadRules() {
        List<File> fileList = getAllRuleFiles();
        for (File file : fileList) {
            String fileName = file.getName();
            String content = encodeToString(file.getPath());
            String contentMd5 = DigestUtils.md5DigestAsHex(content.getBytes());
            String cacheContent = cacheService.get(fileName);
            if (StringUtils.isBlank(cacheContent)){
                // 如果缓存中不存在
                cacheService.put(fileName, content);
                cacheService.put(fileName+Constants.FLAG,Constants.OFF);
            } else{
                String cacheMd5 = DigestUtils.md5DigestAsHex(cacheContent.getBytes());
                if (!cacheMd5.equals(contentMd5)){
                    // 缓存中不存在，或者文件中的值与cache的值不相同，则表示文件存在变化，将新的文件写入缓存中
                    cacheService.put(fileName, content);
                    cacheService.put(fileName+Constants.FLAG,Constants.OFF);
                }
            }
            // 文件中的值与cache的值相同，说明文件没有变化，不做任何处理
        }
    }

    /**
     * 规则文件，决策表解析成字符串
     * @param realPath  决策表路径
     * @return  字符串
     */
    private String encodeToString(String realPath) {
        File file = new File(realPath);
        if (!file.exists()) {
            return null;
        }
        // drl文件
        if (realPath.endsWith(Constants.SUFFIX_DRL)) {
            return read(file);
        }
        InputStream is = null;
        try {
            is = new FileInputStream(file);
        } catch (FileNotFoundException e) {
            log.error("file not fount.");
        }
        // excel文件 xls和xlsx都支持
        // @author <a href="mailto:312182539@qq.com">fbf</a>
        if (realPath.endsWith(Constants.SUFFIX_EXCEL)||realPath.endsWith(Constants.SUFFIX_EXCEL_2007)) {
            return new SpreadsheetCompiler().compile(is, InputType.XLS);
        }
        // csv文件
        if (realPath.endsWith(Constants.SUFFIX_CSV)) {
            return new SpreadsheetCompiler().compile(is, InputType.CSV);
        }
        if (is != null) {
            try {
                is.close();
            } catch (IOException e) {
                log.error("close stream error=>", e);
            }
        }
        return "package src.main.resources;";
    }

    /**
     * 读取drl文件
     */
    private String read(File file) {
        FileInputStream fis = null;
        ByteArrayOutputStream bos = null;
        try {
            fis = new FileInputStream(file);
            bos = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int length;
            while ((length = fis.read(buffer)) != -1) {
                bos.write(buffer, 0, length);
            }
            return bos.toString();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (bos != null) {
                    bos.close();
                }
                if (fis != null) {
                    fis.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
        return null;
    }
}
